package com.ar4j.handler;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;

import net.sf.cglib.proxy.MethodProxy;

import com.ar4j.ActiveRecord;
import com.ar4j.ActiveRecordContext;
import com.ar4j.ActiveRecordMetadata;
import com.ar4j.pagination.PaginationAndSort;
import com.ar4j.sql.ActiveRecordRowMapper;

/**
 * A handler for active record dynamic lookup methods (findBy, findAllBy, countBy, native sql, named queries)
 */
public class LookupMethodHandler implements IMethodHandler {
  private LookupMethodMetadata lookupMetadata;
  
  public LookupMethodHandler(LookupMethodMetadata lookupMetadata) {
    this.lookupMetadata = lookupMetadata;
  }

  @Override
  public Object intercept(ActiveRecordMetadata metadata, ActiveRecordContext context, Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    
    // compose find parameters
    Map<String, Object> parameters = new HashMap<String, Object>();
    parameters.putAll(lookupMetadata.getDefaultParameters());
    int i = 0;
    for(String parameterName : lookupMetadata.getParameters()) {
      parameters.put(parameterName, args[i]);
      i++;
    }

    // get pagination data if there is any
    PaginationAndSort paginationAndSort = PaginationAndSort.NONE;
    if(lookupMetadata.getIsLastArgumentPagination()) {
      paginationAndSort = (PaginationAndSort) args[args.length - 1];
    }
    
    // execute the find or count
    ActiveRecord<?> record = (ActiveRecord<?>) target;
    switch(lookupMetadata.getLookupMethodType()) {
      case COUNT_BY:      return record.count(parameters);
      case FIND_ALL_BY:   return record.find(parameters, paginationAndSort, lookupMetadata.getIsForUpdate());
      case FIND_BY:       return runFindByLookup(record, parameters, paginationAndSort);
      case NATIVE_SQL:    return runNativeSqlLookup(method, metadata, context, lookupMetadata.getSqlQuery(), parameters, paginationAndSort);
      case NAMED_QUERY:   return runNamedQueryLookup(method, metadata, context, lookupMetadata.getQueryName(), parameters, paginationAndSort);
      case EXISTS_BY:      return record.exists(parameters);
        
      default:
        throw new IllegalArgumentException("Unhandled lookup method type: " + lookupMetadata.getLookupMethodType());
    }
    
  }
  
  /**
   * Executes a standard single object dynamic findBy using the given parameters
   */
  private Object runFindByLookup(ActiveRecord<?> record, Map<String, Object> parameters, PaginationAndSort paginationAndSort) {
    Collection<?> results = record.find(parameters, paginationAndSort, lookupMetadata.getIsForUpdate());
    if(results.size() != 1) {
      throw new IncorrectResultSizeDataAccessException("Invalid number of results for a findBy operation", 1, results.size());
    }
    
    return results.iterator().next();
  }
  
  /**
   * Runs a native SQL lookup, determining whether the query is for a long, a single active record,
   * or a collection of active record instances.
   */
  @SuppressWarnings("unchecked")
  private Object runNativeSqlLookup(Method method, ActiveRecordMetadata metadata, ActiveRecordContext context, String sqlQuery,
      Map<String, Object> parameters, PaginationAndSort paginationAndSort) {
    
    MapSqlParameterSource source = new MapSqlParameterSource(parameters);
    if(paginationAndSort.getStartRow() > 0 || paginationAndSort.getMaxRows() < Integer.MAX_VALUE) {
      source.addValue("startRow", paginationAndSort.getStartRow());
      source.addValue("maxRows", paginationAndSort.getMaxRows());
    }
    
    Class<?> returnType = method.getReturnType();
    
    // a dynamic count (similar to countBy), returning a long
    if(Long.class.equals(returnType) || long.class.equals(returnType)) {
      return context.getSimpleTemplate().queryForLong(sqlQuery, source);
      
    } 
    
    // dynamic single active record lookup (similar to findBy) 
    else if (metadata.getBaseClass().isAssignableFrom(returnType)) {
      return context.getSimpleTemplate().queryForObject(sqlQuery, new ActiveRecordRowMapper(metadata, context), source);
      
    } 
    
    // dynamic collection of active record lookup (similar to findAllBy)
    else if(Collection.class.isAssignableFrom(returnType) && 
        metadata.getBaseClass().isAssignableFrom(GenericCollectionTypeResolver.getCollectionReturnType(method))) {
      
      return context.getSimpleTemplate().query(sqlQuery, new ActiveRecordRowMapper(metadata, context), source);
    } 
    
    // unknown return type
    else {
      throw new IllegalArgumentException("Unrecognized return type for a dynamic finder method: " + method.getName() + ", type = " + returnType.getName());
    }
  }
  
  /**
   * Runs a named query lookup. Extracts a SQL query from the named query source and executes the runNativeSqlLookup methdo.
   */
  private Object runNamedQueryLookup(Method method, ActiveRecordMetadata metadata, ActiveRecordContext context, String queryName,
      Map<String, Object> parameters, PaginationAndSort paginationAndSort) {
    
    // make sure we have a parameter source
    if(context.getNamedQuerySource() == null) {
      throw new IllegalArgumentException("Cannot execute a named query dynamic finder without a named query source for method: " + method.getName());
    }
    
    String sqlQuery = context.getNamedQuerySource().findQueryByName(queryName);
    return runNativeSqlLookup(method, metadata, context, sqlQuery, parameters, paginationAndSort);
  }

}
